home *** CD-ROM | disk | FTP | other *** search
/ TPUG - Toronto PET Users Group / TPUG Users Group CD / TPUG Users Group CD.iso / AMIGA / AMICUS / AMICUS05.ADF / IFF / iffbckgr.txt < prev    next >
Text File  |  1986-04-20  |  17KB  |  415 lines

  1.  
  2.  
  3.                 BACKGROUND ON THE EXAMPLE IFF SOURCE CODE
  4.  
  5. Jerry Morrison, 1/30/86
  6.  
  7. The example IFF code is written using a programming style and techniques
  8. that may be unfamiliar to you. So here's a tutorial on "call-back
  9. procedures","enumerators", "interfaces", and "sub-classed structures". I
  10. recommend these programming practices independently of IFF software.
  11.  
  12.  
  13. DEFINITIONS: "CLIENT" VS. "USER"
  14.  
  15. First, some definitions. The word "user" is reserved for a human user of a
  16. software package. That's you and me.
  17.  
  18. A "client" of a software package, on the other hand, is a piece of software
  19. that uses that software package. A program that calls operating system
  20. routines such as "OpenFile" is a client of that operating system.
  21.  
  22.  
  23. CALL-BACK PROCEDURES
  24.  
  25. Consider an operating system subroutine "ListDir" that lists the files in a
  26. disk directory. It might allow you to list just the filenames matching a
  27. pattern like "a*.text". Maybe you can ask it to list just the files created
  28. since yesterday ... or those longer than 2000 bytes. ListDir is a fancy,
  29. general-purpose directory subroutine that lets you pass in a number of
  30. arguments to filter the listing.
  31.  
  32. A C definition might look like:
  33.  
  34.    void ListDir(directory, namePattern, minSize, maxSize, minDate ...); ... {
  35.       for (each file in the directory)
  36.          if ( PatternMatch(namePattern, filename)
  37.                &&  fileSize >= minSize
  38.                &&  fileSize <= maxSize
  39.                &&  fileDate >= minDate
  40.                && ... )
  41.             printf("%s\n", filename);  /* probably fancier than this... */
  42.       }
  43.  
  44. and your call to it:
  45.  
  46.    ListDir(myDir, "a*.text", 0, maxFileSize, date1_1_1900, ...);
  47.  
  48. When you think about it, these filtering arguments make up a
  49. special-purpose "file filtering language". The person who designed this
  50. subroutine "ListDir" might be pretty pleased with his accomplishment. But
  51. in practice he can never put in enough features into this special-purpose
  52. language to satisfy everyone. (You say you need to list just the files
  53. currently open?) And he may have provided a lot of functionality that is
  54. rarely needed. Is this filtering language what he should spending his time
  55. designing, writing, and debugging?
  56.  
  57. A much better technique is to use a "call-back procedure". The concept is
  58. simple: instead of all those filter arguments to ListDir, you pass it a
  59. pointer to a "filter procedure". ListDir simply calls your procedure (via the
  60. pointer) to do the filtering, once per file. It passes each filename to your
  61. "filter proc", which returns "TRUE" to include that file in the listing or
  62. "FALSE" to skip it.
  63.  
  64.    typedef BOOL FilterProc();  /* FilterProc: a BOOL procedure */
  65.  
  66.    void ListDir(directory, filterProc);
  67.          Directory directory;  FilterProc *filterProc;  {
  68.       for (each file in the directory)
  69.          if ( (*filterProc)(filename) )  printf("%s\n", filename);
  70.       }
  71.  
  72. and your code:
  73.  
  74.    BOOL MyFilterProc(filename)  STRING filename;  {
  75.       return(PatternMatch("a*.text", filename));
  76.       }
  77.  
  78.       ...
  79.       ListDir(myDir, MyFilterProc);
  80.  
  81. This technique has many advantages. It gives unlimited flexibility to
  82. ListProc. It means you can use a general-purpose programming language
  83. instead of learning a special-purpose filtering language. It's more
  84. efficient to call a compiled subroutine than to "interpret" the filtering
  85. parameters. And it means you can do anything you want in a filter proc,
  86. from selecting files on the basis of numerology to copying files to backup
  87. tape.
  88.  
  89. In practice, ListDir would have data about each file readily available. So it
  90. should pass this data to the filter proc to save time.
  91.  
  92.  
  93. As Alan Kay once said, "Simple things should be simple and complex things
  94. should be possible."
  95.  
  96.  
  97. STANDARD CALL-BACK PROCEDURE
  98.  
  99. I could extend ListDir to accept a NULL FilterProc pointer to mean "list all
  100. files". More likely, I'd supply a standard call-back procedure "FilterTRUE"
  101. that always returns TRUE. Then ListDir(directory, FilterTRUE) will list all
  102. files with no special test for filterProc == NULL.
  103.  
  104.    BOOL FilterTRUE(filename)  STRING filename;  {
  105.       return(TRUE);
  106.       }
  107.  
  108.  
  109. ENUMERATORS
  110.  
  111. Let's take our ListDir example one step further. Rather than have ListDir
  112. print the selected filenames, have it JUST call your custom proc for every
  113. file. Let your custom proc print the filenames, maybe in your own personal
  114. format. Or maybe have it quietly backup new files, or ask the user which
  115. ones to delete, or ...
  116.  
  117.    typedef CallBackProc(/* filename */);
  118.  
  119.    void ListDir(directory, callBackProc);
  120.          Directory directory;  CallBackProc *callBackProc;  {
  121.       for (each file in the directory)
  122.          (*callBackProc)(filename);
  123.       }
  124.  
  125. and your code:
  126.  
  127.    void MyProc(filename)  STRING filename;  {
  128.       if ( PatternMatch("a*.text", filename) )
  129.          printf("%s\n", filename);
  130.       }
  131.  
  132.       ...
  133.       ListDir(myDir, MyProc);
  134.  
  135. Now we're talking about a full-blown "enumerator". The procedure "ListDir"
  136. is said to "enumerate" all the files in a directory. It "applies" your
  137. call-back procedure to each file. The enumerator scans the directory and
  138. your call-back procedure processes the files. It deals with the internal
  139. directory details and you deal with the printout. A nice separation of
  140. concerns.
  141.  
  142.  
  143. ListDir should come with a standard call-back procedure "PrintFilename"
  144. that lists the filename. By simply passing PrintFilename to ListDir, you
  145. can print a directory. By writing a call-back procedure that selectively
  146. calls the PrintFilename, you can filter the listing.
  147.  
  148.    void PrintFilename(filename)  STRING filename;  {
  149.       printf("%s\n", filename);
  150.       }
  151.  
  152.  
  153. ENUMERATION CONTROL
  154.  
  155. A simple enhancement is to empower the call-back procedure to stop the
  156. enumeration early. That's easy. Have it return "TRUE" to stop. This is very
  157. handy, for example, to quit when you find what you're looking for. Let's
  158. expand this boolean "continue/stop" result into an integer error code.
  159.  
  160.    #define OKAY   0
  161.    #define DONE  -1
  162.    typedef int CallBackProc(/* filename */);
  163.  
  164.    int ListDir(directory, callBackProc);
  165.          Directory directory;  CallBackProc *callBackProc;  {
  166.       int result = OKAY;
  167.       for (each file in the directory) while (result == OKAY)
  168.          result = (*callBackProc)(filename);
  169.       return(result);
  170.       }
  171.  
  172.  
  173. IFF FILE ENUMERATOR
  174.  
  175. Now we'll relate these techniques to the example IFF code. I'm assuming
  176. that you've read "EA IFF 85" Standard for Interchange Format Files. That
  177. memo is available from Commodore as part of their Amiga documentation.
  178. Also ask Commodore for "ILBM" IFF Interleaved Bitmap and the example IFF
  179. source code.
  180.  
  181. Two things make IFF files very flexible for lots of interchange between
  182. programs. First, file formats are independent of RAM formats. That means
  183. you have to do some conversion when you read and write IFF files. Second,
  184. the contents are stored in chunks according to global rules. That means you
  185. have to parse the file, i.e. scan it and react to what's actually there.
  186.  
  187. In the example IFF files IFF.H and IFFR.C, the routines ReadIFF, ReadIList, &
  188. ReadICat are enumeration procedures. ReadIFF scans an IFF file,
  189. enumerating all the "FORM", "LIST", "PROP", and "CAT" chunks encountered.
  190. ReadIList & ReadICat enumerate all the chunks in a LIST and CAT,
  191. respectively.
  192.  
  193. A ClientFrame record is a bundle of pointers to 4 "call-back procedures"
  194. getList, getProp, getForm, and getCat. These 4 procedures are called by
  195. ReadIFF, ReadIList, and ReadICat when the 4 kinds of IFF "groups" are
  196. encountered: "LIST", "PROP", "FORM", or "CAT".
  197.  
  198. These 3 enumerator procedures and 4 client procedures together make up a
  199. reader for IFF files--a very simple recursive descent parser. If you want
  200. to learn more about parsing, a real good place to look is the new edition
  201. "dragon book" by Aho, Ullman, and Sethi.
  202.  
  203. The procedure "SkipGroup" is just a default call-back procedure.
  204.  
  205. The "IFFP" values IFF_OKAY through BAD_IFF are the error codes used by
  206. the IFF enumerators. We use the type "IFFP" to declare variables (and
  207. procedure results) that hold such values. The code "IFF_OKAY" means "AOK;
  208. keep enumerating". The other values mean "stop" for one reason or other.
  209. "IFF_DONE" means "we're all done", while "END_MARK" means "we hit the
  210. end at this nesting level".
  211.  
  212.  
  213. CALL-BACK PROCEDURE STATE
  214.  
  215. ListDir is an enumerator with some internal state--it internally
  216. remembers its place in the directory. It loops over the directory, calling
  217. the client proc once per file. That's fine for some cases and less
  218. convenient for others. Consider this example that just lists the first 10
  219. files:
  220.  
  221.    int count;
  222.  
  223.    int PrintFirst10(filename)  STRING filename;  {
  224.       if (++count > 10)  return(DONE);
  225.       printf("%s\n", filename);
  226.       return(OKAY);
  227.       }
  228.  
  229.    void DoIt();  {
  230.       ...
  231.       count = 0;
  232.       ListDir(myDir, PrintFirst10);
  233.       ...
  234.       }
  235.  
  236. Inherently, the client's code has to be split into code that calls the
  237. enumerator and a call-back procedure. Thus any communication between
  238. the two must be via global variables. In this trivial example, the global
  239. "count" saves state data between calls to PrintFirst10. Often, it's much
  240. more complex. But globals won't work if you need reenterent or recursive
  241. code. We really want "count" to be a local variable of DoIt.
  242.  
  243. Fixing this in Pascal is easy: Define PrintFirst10 as a nested procedure
  244. within DoIt so it can access DoIt's local variables. The manual analog in C
  245. is to redefine the enumerator to pass a raw "client data pointer" straight
  246. through to the call-back procedure. The two client procedures then
  247. communicate through the "client data pointer". DoIt would call
  248. ListDir(myDir, PrintFirst10, &count) which calls PrintFirst10(filename,
  249. &count).
  250.  
  251.    #define OKAY   0
  252.    #define DONE  -1
  253.    typedef int CallBackProc(/* filename, clientData */);
  254.  
  255.    int ListDir(directory, callBackProc, clientData);
  256.          Directory directory; CallBackProc *callBackProc; BYTE *clientData; {
  257.       int result = OKAY;
  258.       for (each file in the directory) while (result == OKAY)
  259.          result = (*callBackProc)(filename, clientData);
  260.       return(result);
  261.       }
  262.  
  263.  
  264. In general, an enumerator is sometimes inconvenient because it takes over
  265. control. Think about this: How could you enumerate two directories in
  266. parallel and copy the newer files from one directory to the other?
  267.  
  268.  
  269. STATELESS ENUMERATOR
  270.  
  271. An alternate form without this disadvantage is the "stateless enumerator".
  272.  
  273. In a stateless enumerator, it's up to the client to keep its place in the
  274. enumeration. Call a procedure like GetNextFilename each time around the
  275. loop.
  276.  
  277.       STRING curFilename = NULL;
  278.       int count = 0;
  279.       do {
  280.          if (++count > 10)  break;  /* stop after 10 files */
  281.          curFilename = GetNextFilename(directory, curFilename);
  282.          if (curFilename == NULL)  break;  /* stop at end of directory */
  283.          printf("%s\n", filename);
  284.          }
  285.  
  286. The stateless enumerator is sometimes better because it puts the client
  287. in control. The above example shows how easy it is to keep state
  288. information between iterations and to stop the enumeration easy. It's also
  289. easy to do things like list two directories in parallel.
  290.  
  291.  
  292. IFF CHUNK ENUMERATOR
  293.  
  294. The following IFFR.C routines make up a stateless IFF chunk enumerator:
  295. OpenRIFF, OpenRGroup, GetChunkHdr and CloseRGroup. Together with
  296. IFFReadBytes, we havm. It handles whatever it finds,
  297. unlike inflexible file readers that demand conformance to a rigid file
  298. format. [Note: This code doesn't check for errors or end-of-context.]
  299.  
  300.    OpenRGroup(..., context);  /* initialize */
  301.    do {
  302.       id = GetChunkHdr(context);  /* get the next chunk's ID */
  303.       switch (id)  {
  304.          case AAAA: {read in an AAAA chunk; break};
  305.          case BBBB: {read in a BBBB chunk; break};
  306.          ...
  307.          default: {};  /* just ignore unrecognized chunks */
  308.          }
  309.    CloseRGroup(context);  /* cleanup */
  310.  
  311. GetChunkHdr reads the next chunk header and returns its chunk ID. You then
  312. dispatch on the chunk ID, that is, switch to a different piece of code for
  313. each type of chunk. If you don't recognize the chunk ID, just keep looping.
  314.  
  315. In each "case:" statement, call IFFReadBytes one or more times to read the
  316. chunk's contents. The readin work you do here depends on the chunk type
  317. and what you need in RAM. Since GetChunkHdr automatically skips to the
  318. start of the next chunk, it doesn't matter if you don't read all the data
  319. bytes.
  320.  
  321. GetChunkHdr does some other things for you automatically. When it reads a
  322. "group" chunk header (a chunk of type "FORM", "LIST", "CAT ", or "PROP") it
  323. automatically reads the subtype ID. That makes it very convenient to just
  324. open the contents of the group chunk as a group context and read the
  325. nested chunks. See the example source program ShowILBM for more about
  326. the relationship between a "GroupContext" and a "ClientFrame".
  327.  
  328. Like all the example IFF code, GetChunkHdr checks for errors. To handle
  329. GetChunkHdr errors, we just add cases to the switch statment. To stop at
  330. end-of-context or an error in a switch case, we add a "while" clause at the
  331. end of the "do" statement.
  332.  
  333.  
  334. CLIENTS, INTERFACES, AND IMPLEMENTORS
  335.  
  336. In the ListDir example, you can see that a lot of flexibility comes from
  337. decoupling the task of tracing through the directory's data structures from
  338. the task of filtering files and printing filenames. This is called
  339. modularity, or simply, dividing a program into parts.
  340.  
  341. Choosing good module boundaries is an art. It has a big impact on a
  342. programmer's ability to coope with lrge programs. Good modularity makes
  343. programs much easier to understand and modify. But this topic would be
  344. another whole tutorial in itself.
  345.  
  346. Just be aware that the example IFF program is divided into various
  347. "modules", each of which implements a different part of the bigger picture.
  348. One such module is the low level IFF reader/writer. It's split into two
  349. files IFFR.C and IFFW.C. Other such modules are the run encoder/decoder
  350. Packer.C and UnPacker.C, and ILBM read/write subroutines ILBMR.C and
  351. ILBMW.C.
  352.  
  353. You'll notice that all three of these "modules" are split into a pair of files.
  354. That's because most linkers aren't fancy enough to automatically eliminate
  355. unused subroutines, e.g. for a program like ShowILBM that reads but doesn't
  356. need the writer code. Also, a program like DeluxePaint wants read and
  357. write code in separate overlays. So think of each pair as a single module.
  358.  
  359. What I want to point out is the basic structure. Each "module" has an
  360. "interface" file (a .H file) that separates the "implementor" .C file(s) from
  361. the "client" programs. This interface is very important, in fact, more
  362. important than the code details inside the .C files. The interfaces for the
  363. above-mentioned modules are called IFF.H, Packer.H, and ILBM.H.
  364.  
  365. Everything about a layer of software that the clients need to know belongs
  366. in its interface: constant and type definitions, extern declarations for the
  367. procedures, and comments. The comments detail the purpose of the module
  368. and each procedure, the procedure arguments, side effects, results, and
  369. error codes, etc. Nothing the clients don't need to know belongs in its
  370. interface: internal implementation details that might change.
  371.  
  372. Thus, the modularization and other important design information is
  373. collected and documented in these interface files. So if you want to
  374. understand what a module does and how to use it, READ ITS INTERFACE.
  375. Don't dive headfirst into the implementation. 
  376.  
  377. Two of the original articles on modular programming are
  378.    D.L. Parnas, "On the Criteria To Be Used in Decomposing Systems into
  379. Modules". Communications of the ACM 15, 12 (Dec. '72), pp 1053-1058.
  380.  
  381.    B. Liskov and S. Zilles, "Programming with Abstract Data Types".
  382. Proceedings ACM SIGPLAN Conference on Very High-Level Languages.
  383. SIGPLAN Notices 9, 4 (April '74), pp 50-59.
  384.  
  385.  
  386. SUBCLASSED STRUCTURES
  387.  
  388. One more technique. In programming, a general-purpose module may define
  389. a structure like ClientFrame. Along comes a more special-purpose program
  390. that needs a structure like it but with specialized fields added on. The
  391. answer is to build a larger structure whose first field is the earlier
  392. structure. This is called "subclassing" a structure, a term that comes from
  393. subclassing in Smalltalk.
  394.  
  395. In the Macintosh(tm) toolbox, the record GrafPort is subclassed to produce
  396. the record WindowRecord, which is subclassed again to produce a
  397. DialogWindow record.
  398.  
  399. Similarly in the example IFF program ShowILBM, the structure ClientFrame
  400. is subclassed to produce the more specialized structure ILBMFrame.
  401.  
  402.    typedef struct {
  403.       ClientFrame clientFrame;
  404.       UBYTE foundBMHD;
  405.       ...
  406.       } ILBMFrame;
  407.  
  408. Since the first field of an ILBMFrame is a ClientFrame, the ShowILBM
  409. procedure ReadPicture can coerce a *ClientFrame pointer to an
  410. *ILBMFrame pointer to pass it to ReadIFF (which knows nothing about
  411. ILBMFrame). When ReadIFF calls back ShowILBM's getForm procedure, we
  412. can coerce it back to an *ILBMFrame pointer. Take a look at ShowILBM to
  413. see how this works.
  414.  
  415.